iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0

前言

早上在思考今天的進度時,發現昨天有個地方做錯了,MAIL 那張表原先有個 STATUS 欄位預計用來存已讀跟未讀的狀態,但是要有這個狀態應該也是每個用戶要有一筆資料,這跟當初設計衝突了,原本是想說只存一筆站內信內容,就可以根據用戶類型發送給全部有這個角色的用戶,這部分是正確的沒錯,但沒有地方存用戶已讀未讀這封信件的狀態,所以今天就要來實現這個擴展。

實作 Lab:根據 RBAC 權限模型發送站內信

今日的實作目標,預計要完成的進度:

  1. 更改 MAIL 表的欄位。
  2. 創建一張新的表。
  3. 保存邏輯優化及擴展。

Alter & Create Table

首先我們先快速把 MAIL 表中用不到的欄位移除,另外還有更新下欄位名稱、類型,這樣關聯到新表時可讀性比較好:

  1. 移除 STATUS 欄位,這個要改到新表。
  2. MAIL_ID 到時要關聯到新表,這樣查詢收件匣時才能查到站內信的內容。
  3. RECEIVER_ROLE 直接存 ROLE 表的 ROLE_ID 就好 → 改欄位名稱 (RECEIVER_ROLE_ID) & 類型。
ALTER TABLE `MAIL` DROP COLUMN STATUS;
ALTER TABLE `MAIL` RENAME COLUMN ID TO MAIL_ID;
ALTER TABLE `MAIL` RENAME COLUMN RECEIVER_ROLE TO RECEIVER_ROLE_ID;
ALTER TABLE `MAIL` CHANGE `RECEIVER_ROLE_ID` `RECEIVER_ROLE_ID` INT NOT NULL ;

接著創建一個新表 MAIL_INBOX 存放用戶的信件:

CREATE CORE_BASE.MAIL_INBOX(
    ID BIGINT PRIMARY KEY AUTO_INCREMENT,
    MAIL_ID BIGINT NOT NULL,
    USER_ID INT NOT NULL,
    STATUS TINYINT DEFAULT 0,
    VERSION INT DEFAULT 0,
    CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UPDATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

業務邏輯

來改寫一下業務邏輯,一樣是同一個 API 但分為兩個部分,站內信的保存跟用戶收件匣的信件保存,用戶收件匣可能會有大量的資料筆數,我們用 Kafka 異步處理這個 Task,雖然用 @Async 也是可以,但之後推送的流程可能也差不多是這樣,所以先保持這個架構,之後再看看。

發送站內信

  1. 保存站內信到資料庫:這邊先檢查一下前端送來的 Role 是否有定義在資料庫中。
  2. 發送 Kafka 到 MAIL_INBOX Topic:保存後再創建一個 MailInboxTO 的 TO 實例將 mailId 跟 roleId 透過 Kafka Producer 異步發送出去。
@Override
@Transactional
public void sendMail(MailSendTO mailSendTO) {
    var mail = new Mail();
    var role = roleRepo.findByRoleType(mailSendTO.getReceiverRole())
            .orElseThrow(() -> new BaseException(StatusCode.REQ_PARAM_ERR, "Role type does not exist"));

    mail.setContent(mailSendTO.getContent());
    mail.setReceiverRoleId(role.getRoleId());
    mailRepo.saveAndFlush(mail);

    var mailInboxTO = MailInboxTO.builder()
            .mailId(mail.getMailId())
            .roleId(mail.getReceiverRoleId())
            .build();

    kafkaSender.send(Topic.MAIL_INBOX, mailInboxTO);
}

用戶收件匣

  1. Kafka Consumer:監聽到 MAIL_INBOX 的 Kafka Message 後轉成 Java 物件調用 mailService.saveUserMailInbox(mail); 開始進行用戶收件匣的信件保存。
@KafkaListener(topics = Topic.MAIL_INBOX)
public void processMailNotification(String content) {
    log.info("Receive topic [{}] and message=[{}]", Topic.MAIL_INBOX, content);
    try {
        var om = new ObjectMapper();
        var mail = om.readValue(content, MailInboxTO.class);
        mailService.saveUserMailInbox(mail);
    } catch (Exception e) {
        log.warn("Failed to process mail task", e);
    }
}
  1. 根據 Role 保存站內信到用戶收件匣:用 roleId 去查 USER_ROLE 這張表,看看這個角色類型的用戶有哪些,再用這個 userId 的 List 去批次保存進收件匣。
@Override
@Transactional
public void saveUserMailInbox(MailInboxTO to) {
    if (to.getMailId() == null || to.getRoleId() == null) {
        log.info("Mail id can not be null");
        return;
    }

    var userIds = userRoleRepo.findUserIdByRoleId(to.getRoleId());
    var mailInboxes = new ArrayList<MailInbox>();
    userIds.forEach(userId -> {
        var mailInbox = new MailInbox();
        mailInbox.setMailId(to.getMailId());
        mailInbox.setUserId(userId);
        mailInboxes.add(mailInbox);
    });

    log.info("Total {} mails inbox", mailInboxes.size());
    mailInboxRepo.saveAll(mailInboxes);
}

總結

今天先收尾到這,完成了表結構的優化,簡易基礎的 CRUD 對於小系統來說算夠用,反正之後會慢慢優化,以及測試效能瓶頸等等。效能測試這部分說實在我還很不熟,平時都是主管會提出效能優化要求,但為了往 Senior 這條路邁進,這是需要主動提升的素養,在倒數幾天一定要有所成長。

另外,今天其實還有查詢用戶收件匣的功能,想說留到明天實作,因為今天還有一點時間我想思考如何規劃 WebSocket 的推送功能,這部分我不太熟悉,睡前再溫習一下這部分,然後明天還可以用查看收件匣作為一個緩衝區,週六週日就能有更多時間可以來實作 WebSocket 的功能。


上一篇
Day 13 | 站內信實作 Lab 第 0 版 | Kafka 測一筆保存
下一篇
Day 15 中場休息一下
系列文
系統設計一招一式:最基本的功練到爛熟就是殺手鐧,從單體架構到分布式系統的 Lab 實作筆記16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言